/*
 * GPL License jazz, goog_pam is (c)Rich Lundeen
 *
 * This is the main module, which is plugged into the pam auth stack.
 *
 * When correctly configured, pam_sm_chauthtok is called when a user changes
 * his/her password. This function grabs user information, and calls
 * goog_access to change the google apps password
 *
 */

#define PAM_SM_PASSWORD
#define PAM_SM_AUTH

#include <security/pam_modules.h>

#include <syslog.h>
#include <pwd.h>
#include <syslog.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <openssl/sha.h>

#include "goog_authuser.h"

#define CONST_ARG const

#define NEW_PASSWORD_PROMPT "New (google) password: "
#define AGAIN_PASSWORD_PROMPT "Re-enger new password: "

unsigned char sha1_passhash[20];
unsigned char sha1_asciipass[41];
char ascii_hexval[16] =   { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                            'A', 'B', 'C', 'D', 'E', 'F'};

void report_error(int errnum) {
  printf("Errnum: %i\n",errnum);
  switch (errnum) {
    case 1:
      syslog(LOG_ERR, "pam_google pass: what the fork? (forking error)");
      break;
    case 2:
      syslog(LOG_ERR, "pam_google pass: Bad usage");
      break;
    case 3:
      syslog(LOG_ERR, "pam_google pass: admin username/password error or network error");
      break;
    case 4:
      syslog(LOG_ERR, "pam_google pass: username does not exist");
      break;
    case 5:
      syslog(LOG_ERR, "pam_google pass: user is not authed");
      break;
    case 6:
      syslog(LOG_ERR, "pam_google pass: configuration file error");
      break;
    case 7:
      syslog(LOG_ERR, "pam_google pass: arguments are too long");
      break;
    case 42:
      syslog(LOG_ERR, "pam_google pass: unhandled error in goog_pass_sock");
      break;
    default:
      syslog(LOG_ERR, "pam_google pass: unhandled exception, number: %i", errnum);
  }
}

PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) {
  int rc = PAM_SUCCESS;
  char *username, *curpass = NULL, *newpass = NULL;

  struct pam_message msg, *pmsg;
  struct pam_response *resp;
  struct pam_conv *appconv;

  int debug = 0;
  int use_first_pass = 0, try_first_pass = 0, no_warn = 0;
  int i;

  //goog_access is a wrapper program for goog_sock - it authenticates and escalates privs
  char *netprog = "/etc/security/goog_pam/goog_access";
  char *progarg0 = "goog_access";


  int execreturn;
  pid_t PID;


  //get pam options
  for (i=0; i < argc; i++) {
    if (!strcmp (argv[i], "use_first_pass")) {
      use_first_pass = 1;
    }
    else if (!strcmp (argv[i], "try_first_pass")) {
      try_first_pass = 1;
    }
    else if (!strcmp (argv[i], "debug")) {
      debug = 1;
    }
  }

  rc = pam_get_item (pamh, PAM_CONV, (CONST_ARG void **) &appconv);
  if (rc != PAM_SUCCESS) {
    syslog(LOG_ERR, "pam_google pass: strange, cannot retrieve pam handle");
    return rc;
  }

  //get the username
  rc = pam_get_user (pamh, (CONST_ARG char **) &username, NULL);
  if (rc != PAM_SUCCESS) {
    syslog(LOG_ERR, "pam_google pass: cannot determine username");
    return rc;
  }

  if (username == NULL) {
    syslog(LOG_ERR, "pam_google pass: cannot determine username");
    return PAM_USER_UNKNOWN;
  }

  //somehow, it might be nice to authenticate old password, but we don't have anything to compare it against locally
  //plus it might not be convenient...

  //For now, this must be done elsewhere... eg pam_unix, pam_ldap
  //note the old google password is not checked to be correct!

  //get the new password
  
  if (try_first_pass || use_first_pass) {
    if (pam_get_item (pamh, PAM_AUTHTOK, (CONST_ARG void **) &newpass) != PAM_SUCCESS) {
      newpass = NULL;
    }
    if ((use_first_pass|| try_first_pass) && newpass == NULL) {
      return PAM_AUTHTOK_RECOVERY_ERR;
    }
  }
 
  //in most cases the first_pass should probably be used since there is no authentication here
  //if wanted, an authentication flag can be added to the conf file, but it is not an argument
  //(and really can't be because of the nature of the wrapper)

  if (newpass == NULL ) {
    pmsg = &msg;
    msg.msg_style = PAM_PROMPT_ECHO_OFF;
    msg.msg = NEW_PASSWORD_PROMPT;
    resp = NULL;

    rc = appconv->conv(1, (CONST_ARG struct pam_message **) &pmsg, &resp, appconv->appdata_ptr);
    if (rc != PAM_SUCCESS)
      return rc;
    newpass = resp->resp;
    free(resp);
  }

  //hash the password using sha1, which is the strongest hash supported by google apps api
  //the only other hash available is md5, I suppose this might be made to be configurable
  SHA1(newpass, strlen(newpass), sha1_passhash);
  //store the hash as ascii, necessary? for communication with google
  for(i=0; i<20; i++) {
    sha1_asciipass[i*2] = ascii_hexval[((sha1_passhash[i] >> 4) & 0xF)];
    sha1_asciipass[(i*2) + 1] = ascii_hexval[(sha1_passhash[i]) & 0x0F];
  }
  sha1_asciipass[40] = (char)0;

  //this updates the google password essentially doing a system call
  //fork and execv are prefered beacuse goog_access is setuid as user goog_pam
  //see man system as to why system shouldn't be used

  PID = fork();
  if (PID == 0) {
    //child
    char* argvarray[4] = {progarg0, username, sha1_asciipass, (char) 0};
    execv(netprog, argvarray);
    if (debug == 1)
      printf("forking failure\n");
    report_error(1);   
    return PAM_CRED_ERR;
  }
  else if (PID < 0) {
    if (debug == 1)
      printf("unexpected error\n");
    report_error(1);   
    return PAM_CRED_ERR;
  }

  wait(&execreturn);
  //make sure this does exit properly and isn't killed
  if (WIFEXITED(execreturn)) {
    rc = WEXITSTATUS(execreturn);
  }  
  else {
    report_error(1);
    return PAM_CRED_ERR;
  }

  if (debug == 1)
    printf("goog_change_pass exited with status code %i\n", rc);

  if(rc != 0) {
    report_error(rc);   
    if (debug == 1)
      printf("google password not changed\n");
    return PAM_CRED_ERR;
  }
  else {
    syslog(LOG_INFO, "pam_google pass: %s google password changed successfully", username);
  }  

  return PAM_SUCCESS;
}

PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
  int rc;
  const char *username;
  char *passwrd;
  int use_first_pass = 0, try_first_pass = 0, ignore_flags = 0;

  struct pam_message msg, *pmsg;
  struct pam_response *resp;
  struct pam_conv *appconv;

  int i;
  //TODO fix this
  char* domain = "coe.isu.edu";
  
  //get options
  for (i = 0; i < argc; i++) {
    if (!strcmp (argv[i], "use_first_pass"))
      use_first_pass = 1;
    else if (!strcmp (argv[i], "try_first_pass"))
      try_first_pass = 1;
    else if (!strcmp (argv[i], "ignore_flags"))
      ignore_flags = 1;
    else
      syslog (LOG_ERR, "pam_goog: illegal option %s", argv[i]);
  }
  rc = pam_get_item (pamh, PAM_CONV, (CONST_ARG void **) &appconv);
  if (rc != PAM_SUCCESS) {
    syslog(LOG_ERR, "pam_google auth: cannot retrieve pam handle");
    return rc;
  }

  printf("In authentication shebang\n");

  //get username 
  rc = pam_get_user (pamh, (CONST_ARG char **) &username, NULL);
  if (rc != PAM_SUCCESS)
    return rc;

  printf("username is %s\n", username); 

  //get password
  if (try_first_pass || use_first_pass) {
    if (pam_get_item (pamh, PAM_AUTHTOK, (CONST_ARG void **) &passwrd) != PAM_SUCCESS) {
      passwrd = NULL;
    }
    if ((use_first_pass|| try_first_pass) && passwrd == NULL) {
      return PAM_AUTHTOK_RECOVERY_ERR;
    }
  }

  //if password wasn't previously obtained, get it here
  if(passwrd == NULL) {
    pmsg = &msg;
    msg.msg_style = PAM_PROMPT_ECHO_OFF;
    msg.msg = NEW_PASSWORD_PROMPT;
    resp = NULL;

    rc = appconv->conv(1, (CONST_ARG struct pam_message **) &pmsg, &resp, appconv->appdata_ptr);
    if (rc != PAM_SUCCESS)
      return rc;
    passwrd = resp->resp;
    free(resp);
  }
  printf("password is: %s\n", passwrd);


  //get domain in python? 
  //authuser(char* username, char* domain, char* password); 
  return PAM_SUCCESS;
}
